package edu.uoc.sd.fr14.impl;

import edu.uoc.sd.fr14.actors.Clock;
import edu.uoc.sd.fr14.actors.Cluster;
import edu.uoc.sd.fr14.actors.Server;
import edu.uoc.sd.fr14.imesg.ConfirmedMessage;
import edu.uoc.sd.fr14.model.Company;
import edu.uoc.sd.fr14.model.CustomerId;
import edu.uoc.sd.fr14.comp.ClockComponent;
import edu.uoc.sd.fr14.model.Flight;
import edu.uoc.sd.fr14.model.FlightId;
import edu.uoc.sd.fr14.model.Log;
import edu.uoc.sd.fr14.model.Nseq;
import edu.uoc.sd.fr14.model.NseqTracker;
import edu.uoc.sd.fr14.model.OpId;
import edu.uoc.sd.fr14.model.Op;
import edu.uoc.sd.fr14.model.ProxyId;
import edu.uoc.sd.fr14.model.Quorum;
import edu.uoc.sd.fr14.model.Role;
import edu.uoc.sd.fr14.model.ServerId;
import edu.uoc.sd.fr14.model.ServerState;
import edu.uoc.sd.fr14.model.SubLog;
import edu.uoc.sd.fr14.model.AbstractServer;
import edu.uoc.sd.fr14.imesg.QueriedMessage;
import edu.uoc.sd.fr14.imesg.SeatMessage;
import edu.uoc.sd.fr14.mesg.TargetId;
import edu.uoc.sd.fr14.imesg.PrepareMessage;
import edu.uoc.sd.fr14.imesg.PreparedMessage;
import edu.uoc.sd.fr14.imesg.ApplyMessage;
import edu.uoc.sd.fr14.imesg.AbortMessage;
import edu.uoc.sd.fr14.imesg.ElectMessage;
import edu.uoc.sd.fr14.imesg.RecoverMessage;
import edu.uoc.sd.fr14.mesg.TimeoutElectionBeginMessage;

public class StudentServer extends AbstractServer {

    private Cluster cluster;
    private Company company;
    private Log log;
    private NseqTracker nseqTracker;
    private Role role;
    private ServerId serverId;
    
    public StudentServer(ServerState serverState, Cluster cluster){
        super(serverState, cluster);
        this.cluster=cluster;
        this.company=serverState.getCompany();
        this.log=serverState.getLog();
        this.nseqTracker = serverState.getNseqTracker();
        this.role = serverState.getRole();
        this.serverId = role.getMyId();
    }
    
    public void confirm(FlightId flight, CustomerId customer) {
        Flight f = company.getFlight(flight);
        boolean confirmed = f.confirm(customer);
        cluster.sendToProxy(new ConfirmedMessage(customer, flight, customer, confirmed));
    }
    
    public void confirmed(FlightId flight, CustomerId customer, boolean value) {
        //Confirmed se ejecuta cuando se le da el mensaje de OK cuando todo ha funcionado. 
        //Se le envia el customer que inició la petición de Seat
        cluster.sendToCustomer(new ConfirmedMessage(customer, flight, customer, value));
        Clock clock = cluster.getClock();
        clock.cancelSeatTo(flight, customer, serverId);
    }
    
    public void apply(Nseq nseq, SubLog ops, ServerId from) {
        if (nseqTracker.isAccepted(nseq,from)){
            nseqTracker.advance(ops, from);
            ops.apply(company);
        } else {
            Nseq nseq2 = nseqTracker.getCurrent(role.getMyId());
            cluster.sendToServer(new RecoverMessage(from,nseq2, role.isLeader(),role.getMyId()));
            
        }
    }

    public void prepare(OpId op, ServerId from) { //enviamos el confirm al líder 
        if(role.isLeader()){
            cluster.sendToServer(new AbortMessage(from));
        } else {
            role.setLeader(from);
            cluster.sendToServer(new PreparedMessage(from,op));
        }        
    }
        
    public void prepared(OpId op) {
        Clock reloj = cluster.getClock();
        if(role.isLeaderKnown()){ //si no existe líder no tenemos que mirar nada más ya que no podemos responderle.
            Quorum myQuorum = cluster.getQuorum();
            Op operacio = log.prepared(op,myQuorum);
            if (operacio != null){ //hay quorum
                //recupero datos necesarios para el confirmed posterior
                FlightId f = operacio.getFlight();
                Flight vuelo = company.getFlight(f);
                CustomerId idc = operacio.getCustomer();
                if ((vuelo.confirm(idc)) == true || (vuelo.query() != 0) ){ //revisamos si hay plazas antes de confirmar la operación.
                Nseq nsequ = log.executeOp(operacio, company);
                    if (nsequ != null){
                        SubLog ops = log.recover(nsequ);
                        for (ServerId target : cluster.getServers()) {  //enviamos mensaje a los proxis
                        if (role.getMyId() != target) cluster.sendToServer(new ApplyMessage(target,nsequ,ops,role.getMyId()));
                        }
                    } 
                    cluster.sendToProxy(new ConfirmedMessage(operacio.getProxy(),f,idc,true));
                } else {
                    cluster.sendToProxy(new ConfirmedMessage(operacio.getProxy(),f,idc,false));
                }
               reloj.cancelPrepareTo(op, serverId);        
            }
        }        
    }

    public void query(FlightId flight, CustomerId from) {
        Flight f = company.getFlight(flight);
        int seats = f.query();
        cluster.sendToProxy(new QueriedMessage(from,flight, seats));               
    }

    public void seat(FlightId flight, CustomerId customer, ProxyId from) {
        Flight f = company.getFlight(flight);
        Clock reloj = cluster.getClock();
       
        if (role.isLeaderKnown()){ //si existe un líder podemos hacer seat sino hay que hacer elecciones
            if (role.isLeader()){ //somos los líderes por lo que hacemos nosotros el seat  
                OpId opid = log.annotate(flight,customer, from);//anotamos la operacion para el log.
                if (opid != null){ //verificamos que hemos podido anotar la operación
                    int seats = f.query(); //miro si hay asientos libres
                    if ((f.confirm(customer)) == false && (seats == 0) ){
                        cluster.sendToCustomer(new ConfirmedMessage(customer,flight,customer,false));
                        reloj.setPrepareTo(opid, serverId);

                    } else {
                        reloj.setPrepareTo(opid, serverId);

                         for (ServerId target : cluster.getServers()) {  //enviamos mensaje a los proxis
                            if (role.getMyId() != target) cluster.sendToServer(new PrepareMessage(target, opid, role.getMyId())); 
                         }
                    }
                }
            } else {
               reloj.setSeatTo(flight, customer, serverId);
                cluster.sendToServer(new SeatMessage(role.getLeader(),flight,customer,role.getMyId()));
            }
        } else {
            role.beginElections();
        }       
    }
        
    public void elect(ServerId from) {
        cluster.sendToServer(new AbortMessage(from));
        Clock reloj = cluster.getClock();
        reloj.setElectionTo(role.getMyId());
    }

    public void timeoutElectionBegin() {
        log.cancelOps();
        role.beginElections();
        for (ServerId target : cluster.getServers()) {  //enviar mensaje a servers de >id que la propia
            if (target.isGreaterThan(role.getMyId())) {
                cluster.sendToServer(new ElectMessage(target, role.getMyId()));
            }
        }
    }

    public void timeoutElectionEnd() {
        if (role.isDoingElections()){
           Nseq nseq2 = nseqTracker.getCurrent(role.getMyId());
            role.becomeLeader();
            role.endElections();
            for (ServerId target : cluster.getServers()) {  //enviamos mensaje a todos los servidores por tal de tener updateado el
                //subLog y para que el resto de servidores sepan que somos el nuevo líder.
                        if (role.getMyId() != target) cluster.sendToServer(new RecoverMessage(target,nseq2, true,role.getMyId()));
            }
        }
    }

    public void synchronize() {
        for (ServerId target : cluster.getServers()) {  
            if (target != role.getMyId()) {
            Nseq nseq = nseqTracker.getCurrent(target);
            cluster.sendToServer(new RecoverMessage(target, nseq, role.isLeader(), role.getMyId()));
                
            }
        }
    }

    public void abort() {
        log.cancelOps();
        role.cancelElections();
        if (role.isLeader()) role.removeLeader();
        for (ServerId target : cluster.getServers()) {  
            if (target != role.getMyId()) {
                cluster.sendToServer(new AbortMessage(target));
            }
        }
    }

    public void recover(Nseq nseq, boolean leader, ServerId from) {
        //recover nos devuelve ya el log entero por lo que podemos pasarselo directamente sin necesidad de hacerlo entrada a entrada
        Nseq aux = nseqTracker.getCurrent(from);
        if(leader) {
            role.setLeader(from);
        }
        SubLog ops = log.recover(aux);
        cluster.sendToServer(new ApplyMessage(from,aux,ops,role.getMyId()));
    }

    
    }
